<?php

/*
 * This file is part of the Zephir.
 *
 * (c) Phalcon Team <team@zephir-lang.com>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Zephir;

use Zephir\Documentation\Docblock;
use Zephir\Documentation\DocblockParser;
use Zephir\Exception\CompilerException;
use Zephir\Exception\InvalidArgumentException;

/**
 * Zephir\ClassDefinition.
 *
 * Represents a class/interface and their properties and methods.
 */
final class ClassDefinition
{
    /** @var string */
    protected $namespace;

    /** @var string */
    protected $name;

    /** @var string */
    protected $shortName;

    /** @var string */
    protected $type = 'class';

    /** @var string */
    protected $extendsClass;

    /** @var array */
    protected $interfaces;

    /** @var bool */
    protected $final;

    /** @var bool */
    protected $abstract;

    /** @var bool */
    protected $external = false;

    /** @var ClassDefinitionRuntime|ClassDefinition */
    protected $extendsClassDefinition;

    /** @var ClassDefinition[] */
    protected $implementedInterfaceDefinitions = [];

    /** @var ClassProperty[] */
    protected $properties = [];

    /** @var ClassConstant[] */
    protected $constants = [];

    /** @var ClassMethod[] */
    protected $methods = [];

    /** @var string */
    protected $docBlock;

    /** @var Docblock */
    protected $parsedDocblock;

    /** @var int */
    protected $dependencyRank = 0;

    /** @var array */
    protected $originalNode;

    /** @var EventsManager */
    protected $eventsManager;

    /** @var bool */
    protected $isBundled = false;

    /** @var AliasManager */
    protected $aliasManager = null;

    /**
     * Whether the constructor was generated by zephir
     * (-> no constructor existed previously).
     *
     * @var bool
     */
    protected $isGeneratedConstructor = false;

    /** @var Compiler */
    protected $compiler;

    /**
     * ClassDefinition.
     *
     * @param string      $namespace
     * @param string      $name
     * @param string|null $shortName
     */
    public function __construct(string $namespace, string $name, string $shortName = null)
    {
        $this->namespace = $namespace;
        $this->name = $name;
        $this->shortName = $shortName ?: $name;

        $this->eventsManager = new EventsManager();
    }

    /**
     * Sets if the class is internal or not.
     *
     * @param bool $isBundled
     */
    public function setIsBundled($isBundled)
    {
        $this->isBundled = $isBundled;
    }

    /**
     * Returns whether the class is bundled or not.
     *
     * @return bool
     */
    public function isBundled()
    {
        return $this->isBundled;
    }

    /**
     * Sets if the class constructor was generated by zephir.
     *
     * @param bool $isGeneratedConstructor
     */
    public function setIsGeneratedConstructor($isGeneratedConstructor)
    {
        $this->isGeneratedConstructor = $isGeneratedConstructor;
    }

    /**
     * Returns whether the constructor was generated by zephir.
     *
     * @return bool
     */
    public function isGeneratedConstructor()
    {
        return $this->isGeneratedConstructor;
    }

    /**
     * Sets whether the class is external or not.
     *
     * @param bool $isExternal
     */
    public function setIsExternal($isExternal)
    {
        $this->external = $isExternal;
    }

    /**
     * Returns whether the class is internal or not.
     *
     * @return bool
     */
    public function isExternal()
    {
        return $this->external;
    }

    /**
     * Get eventsManager for class definition.
     *
     * @return EventsManager
     */
    public function getEventsManager()
    {
        return $this->eventsManager;
    }

    /**
     * Set the class' type (class/interface).
     *
     * @param string $type
     */
    public function setType($type)
    {
        $this->type = $type;
    }

    /**
     * Returns the class type.
     *
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Returns the class name.
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Returns the class name without namespace.
     *
     * @return string
     */
    public function getShortName()
    {
        return $this->shortName;
    }

    /**
     * Check if the class definition correspond to an interface.
     *
     * @return bool
     */
    public function isInterface()
    {
        return 'interface' == $this->type;
    }

    /**
     * Sets if the class is final.
     *
     * @param bool $final
     */
    public function setIsFinal($final)
    {
        $this->final = (bool) $final;
    }

    /**
     * Sets if the class is final.
     *
     * @param bool $abstract
     */
    public function setIsAbstract($abstract)
    {
        $this->abstract = (bool) $abstract;
    }

    /**
     * Checks whether the class is abstract or not.
     *
     * @return bool
     */
    public function isAbstract()
    {
        return $this->abstract;
    }

    /**
     * Checks whether the class is abstract or not.
     *
     * @return bool
     */
    public function isFinal()
    {
        return $this->final;
    }

    /**
     * Returns the class name including its namespace.
     *
     * @return string
     */
    public function getCompleteName()
    {
        return $this->namespace.'\\'.$this->shortName;
    }

    /**
     * Return the class namespace.
     *
     * @return string
     */
    public function getNamespace()
    {
        return $this->namespace;
    }

    /**
     * Set the original node where the class was declared.
     *
     * @param array $originalNode
     */
    public function setOriginalNode(array $originalNode)
    {
        $this->originalNode = $originalNode;
    }

    /**
     * Sets the extended class.
     *
     * @param string $extendsClass
     */
    public function setExtendsClass($extendsClass)
    {
        $this->extendsClass = $extendsClass;
    }

    /**
     * Sets the implemented interfaces.
     *
     * @param array $implementedInterfaces
     */
    public function setImplementsInterfaces(array $implementedInterfaces)
    {
        $interfaces = [];
        foreach ($implementedInterfaces as $implementedInterface) {
            $interfaces[] = $implementedInterface['value'];
        }

        $this->interfaces = $interfaces;
    }

    /**
     * Returns the extended class.
     *
     * @return string
     */
    public function getExtendsClass()
    {
        return $this->extendsClass;
    }

    /**
     * Returns the implemented interfaces.
     *
     * @return array
     */
    public function getImplementedInterfaces()
    {
        return $this->interfaces;
    }

    /**
     * Sets the class definition for the extended class.
     *
     * @param ClassDefinitionRuntime|ClassDefinition $classDefinition
     */
    public function setExtendsClassDefinition($classDefinition)
    {
        $this->extendsClassDefinition = $classDefinition;
    }

    /**
     * Returns the class definition related to the extended class.
     *
     * @return ClassDefinition|ClassDefinitionRuntime
     */
    public function getExtendsClassDefinition()
    {
        if (!$this->extendsClassDefinition && $this->extendsClass) {
            if ($this->compiler) {
                $this->setExtendsClassDefinition($this->compiler->getClassDefinition($this->extendsClass));
            }
        }

        return $this->extendsClassDefinition;
    }

    /**
     * Sets the class definition for the implemented interfaces.
     *
     * @param ClassDefinition[] $implementedInterfaceDefinitions
     */
    public function setImplementedInterfaceDefinitions(array $implementedInterfaceDefinitions)
    {
        $this->implementedInterfaceDefinitions = $implementedInterfaceDefinitions;
    }

    /**
     * Returns the class definition for the implemented interfaces.
     *
     * @return ClassDefinition[]
     */
    public function getImplementedInterfaceDefinitions()
    {
        return $this->implementedInterfaceDefinitions;
    }

    /**
     * Calculate the dependency rank of the class based on its dependencies.
     *
     * @return ClassDefinition[]
     */
    public function getDependencies()
    {
        $dependencies = [];
        if ($this->extendsClassDefinition && $this->extendsClassDefinition instanceof self) {
            $dependencies[] = $this->extendsClassDefinition;
        }

        foreach ($this->implementedInterfaceDefinitions as $interfaceDefinition) {
            if ($interfaceDefinition instanceof self) {
                $dependencies[] = $interfaceDefinition;
            }
        }

        return $dependencies;
    }

    /**
     * A class definition calls this method to mark this class as a dependency of another.
     *
     * @param int $rank
     */
    public function increaseDependencyRank($rank)
    {
        $this->dependencyRank += ($rank + 1);
    }

    /**
     * Returns the dependency rank for this class.
     *
     * @return int
     */
    public function getDependencyRank()
    {
        return $this->dependencyRank;
    }

    /**
     * Sets the class/interface docBlock.
     *
     * @param string $docBlock
     */
    public function setDocBlock($docBlock)
    {
        $this->docBlock = $docBlock;
    }

    /**
     * Returns the class/interface docBlock.
     *
     * @return string
     */
    public function getDocBlock()
    {
        return $this->docBlock;
    }

    /**
     * Returns the parsed docBlock.
     *
     * @return DocBlock|null
     */
    public function getParsedDocBlock()
    {
        if (!$this->parsedDocblock) {
            if (\strlen($this->docBlock) > 0) {
                $parser = new DocblockParser('/'.$this->docBlock.'/');
                $this->parsedDocblock = $parser->parse();
            } else {
                return null;
            }
        }

        return $this->parsedDocblock;
    }

    /**
     * Adds a property to the definition.
     *
     * @param ClassProperty $property
     *
     * @throws CompilerException
     */
    public function addProperty(ClassProperty $property)
    {
        if (isset($this->properties[$property->getName()])) {
            throw new CompilerException("Property '".$property->getName()."' was defined more than one time", $property->getOriginal());
        }

        $this->properties[$property->getName()] = $property;
    }

    /**
     * Adds a constant to the definition.
     *
     * @param ClassConstant $constant
     *
     * @throws CompilerException
     */
    public function addConstant(ClassConstant $constant)
    {
        if (isset($this->constants[$constant->getName()])) {
            throw new CompilerException("Constant '".$constant->getName()."' was defined more than one time");
        }

        $this->constants[$constant->getName()] = $constant;
    }

    /**
     * Checks if a class definition has a property.
     *
     * @todo Duplicates getProperty
     *
     * @deprecated
     *
     * @param string $name
     *
     * @return bool
     */
    public function hasProperty($name)
    {
        if (isset($this->properties[$name])) {
            return true;
        } else {
            $extendsClassDefinition = $this->getExtendsClassDefinition();
            if ($extendsClassDefinition) {
                if ($extendsClassDefinition->hasProperty($name)) {
                    return true;
                }
            }

            return false;
        }
    }

    /**
     * Returns a method definition by its name.
     *
     * @param string $propertyName
     *
     * @return bool|ClassProperty
     */
    public function getProperty($propertyName)
    {
        if (isset($this->properties[$propertyName])) {
            return $this->properties[$propertyName];
        }

        $extendsClassDefinition = $this->getExtendsClassDefinition();
        if ($extendsClassDefinition) {
            return $extendsClassDefinition->getProperty($propertyName);
        }

        return false;
    }

    /**
     * Checks if class definition has a property.
     *
     * @param string $name
     *
     * @return bool
     */
    public function hasConstant($name)
    {
        if (isset($this->constants[$name])) {
            return true;
        }

        $extendsClassDefinition = $this->getExtendsClassDefinition();
        if ($extendsClassDefinition) {
            if ($extendsClassDefinition->hasConstant($name)) {
                return true;
            }
        }

        /*
         * Check if constant is defined in interfaces
         */
        return $this->hasConstantFromInterfaces($name);
    }

    /**
     * Returns a constant definition by its name.
     *
     * @param string $constantName
     *
     * @return bool|ClassConstant
     *
     * @throws InvalidArgumentException
     */
    public function getConstant($constantName)
    {
        if (!\is_string($constantName)) {
            throw new InvalidArgumentException('$constantName must be string type');
        }

        if (empty($constantName)) {
            throw new InvalidArgumentException('$constantName must not be empty: '.$constantName);
        }

        if (isset($this->constants[$constantName])) {
            return $this->constants[$constantName];
        }

        $extendsClassDefinition = $this->getExtendsClassDefinition();
        if ($extendsClassDefinition) {
            if ($extendsClassDefinition->hasConstant($constantName)) {
                return $extendsClassDefinition->getConstant($constantName);
            }
        }

        /*
         * Gets constant from interfaces
         */
        return $this->getConstantFromInterfaces($constantName);
    }

    /**
     * Adds a method to the class definition.
     *
     * @param ClassMethod $method
     * @param array       $statement
     *
     * @throws CompilerException
     */
    public function addMethod(ClassMethod $method, $statement = null)
    {
        $methodName = strtolower($method->getName());
        if (isset($this->methods[$methodName])) {
            throw new CompilerException("Method '".$method->getName()."' was defined more than one time", $statement);
        }

        $this->methods[$methodName] = $method;
    }

    /**
     * Updates an existing method definition.
     *
     * @param ClassMethod $method
     * @param array       $statement
     *
     * @throws CompilerException
     */
    public function updateMethod(ClassMethod $method, $statement = null)
    {
        $methodName = strtolower($method->getName());
        if (!isset($this->methods[$methodName])) {
            throw new CompilerException("Method '".$method->getName()."' does not exist", $statement);
        }

        $this->methods[$methodName] = $method;
    }

    /**
     * Returns all properties defined in the class.
     *
     * @return ClassProperty[]
     */
    public function getProperties()
    {
        return $this->properties;
    }

    /**
     * Returns all constants defined in the class.
     *
     * @return ClassConstant[]
     */
    public function getConstants()
    {
        return $this->constants;
    }

    /**
     * Returns all methods defined in the class.
     *
     * @return ClassMethod[]
     */
    public function getMethods()
    {
        return $this->methods;
    }

    /**
     * Checks if the class implements an specific name.
     *
     * @param string $methodName
     *
     * @return bool
     */
    public function hasMethod($methodName)
    {
        $methodNameLower = strtolower($methodName);
        foreach ($this->methods as $name => $method) {
            if ($methodNameLower == $name) {
                return true;
            }
        }

        $extendsClassDefinition = $this->getExtendsClassDefinition();
        if ($extendsClassDefinition instanceof ClassDefinitionRuntime) {
            try {
                $extendsClassDefinition = $this->compiler->getInternalClassDefinition(
                    $extendsClassDefinition->getName()
                );
            } catch (\ReflectionException $e) {
                // Do nothing
                return false;
            }
        }

        while ($extendsClassDefinition instanceof self) {
            if ($extendsClassDefinition->hasMethod($methodName)) {
                return true;
            }

            $extendsClassDefinition = $extendsClassDefinition->getExtendsClassDefinition();
        }

        return false;
    }

    /**
     * Returns a method by its name.
     *
     * @param string $methodName
     * @param bool   $checkExtends
     *
     * @return bool|ClassMethod
     */
    public function getMethod($methodName, $checkExtends = true)
    {
        $methodNameLower = strtolower($methodName);
        foreach ($this->methods as $name => $method) {
            if ($methodNameLower == $name) {
                return $method;
            }
        }

        if (!$checkExtends) {
            return false;
        }

        $extendsClassDefinition = $this->getExtendsClassDefinition();
        if ($extendsClassDefinition instanceof self) {
            if ($extendsClassDefinition->hasMethod($methodName)) {
                return $extendsClassDefinition->getMethod($methodName);
            }
        }

        return false;
    }

    /**
     * Set a method and its body.
     *
     * @param $methodName
     * @param ClassMethod $method
     */
    public function setMethod($methodName, ClassMethod $method)
    {
        $this->methods[$methodName] = $method;
    }

    /**
     * Sets class methods externally.
     *
     * @param array $methods
     */
    public function setMethods($methods)
    {
        $this->methods = $methods;
    }

    /**
     * Tries to find the most similar name.
     *
     * @param string $methodName
     *
     * @return bool|string
     */
    public function getPossibleMethodName($methodName)
    {
        $methodNameLower = strtolower($methodName);

        foreach ($this->methods as $name => $method) {
            if (metaphone($methodNameLower) == metaphone($name)) {
                return $method->getName();
            }
        }

        $extendsClassDefinition = $this->extendsClassDefinition;
        if ($extendsClassDefinition) {
            return $extendsClassDefinition->getPossibleMethodName($methodName);
        }

        return false;
    }

    /**
     * Returns the name of the zend_class_entry according to the class name.
     *
     * @param CompilationContext $compilationContext
     *
     * @throws Exception
     *
     * @return string
     */
    public function getClassEntry(CompilationContext $compilationContext = null)
    {
        if ($this->external) {
            if (!\is_object($compilationContext)) {
                throw new Exception('A compilation context is required');
            }

            $this->compiler = $compilationContext->compiler;

            /*
             * Automatically add the external header
             */
            $compilationContext->headersManager->add($this->getExternalHeader(), HeadersManager::POSITION_LAST);
        }

        return strtolower(str_replace('\\', '_', $this->namespace).'_'.$this->name).'_ce';
    }

    /**
     * Returns a valid namespace to be used in C-sources.
     *
     * @return string
     */
    public function getCNamespace()
    {
        return str_replace('\\', '_', $this->namespace);
    }

    /**
     * Returns a valid namespace to be used in C-sources.
     *
     * @return string
     */
    public function getNCNamespace()
    {
        return str_replace('\\', '\\\\', $this->namespace);
    }

    /**
     * Class name without namespace prefix for class registration.
     *
     * @param string $namespace
     *
     * @return string
     */
    public function getSCName($namespace)
    {
        return str_replace($namespace.'_', '', strtolower(str_replace('\\', '_', $this->namespace).'_'.$this->name));
    }

    /**
     * Returns an absolute location to the class header.
     *
     * @return string
     */
    public function getExternalHeader()
    {
        $parts = explode('\\', $this->namespace);

        return 'ext/'.strtolower($parts[0].\DIRECTORY_SEPARATOR.str_replace('\\', \DIRECTORY_SEPARATOR, $this->namespace).\DIRECTORY_SEPARATOR.$this->name).'.zep';
    }

    /**
     * Checks if a class implements an interface.
     *
     * @param ClassDefinition $classDefinition
     * @param ClassDefinition $interfaceDefinition
     *
     * @throws CompilerException
     */
    public function checkInterfaceImplements(self $classDefinition, self $interfaceDefinition)
    {
        foreach ($interfaceDefinition->getMethods() as $method) {
            if (!$classDefinition->hasMethod($method->getName())) {
                throw new CompilerException(
                    sprintf(
                        'Class %s must implement a method called: "%s" as requirement of interface: "%s"',
                        $classDefinition->getCompleteName(),
                        $method->getName(),
                        $interfaceDefinition->getCompleteName()
                    )
                );
            }

            if (!$method->hasParameters()) {
                continue;
            }

            $implementedMethod = $classDefinition->getMethod($method->getName());
            if ($implementedMethod->getNumberOfRequiredParameters() > $method->getNumberOfRequiredParameters() ||
                $implementedMethod->getNumberOfParameters() < $method->getNumberOfParameters()
            ) {
                throw new CompilerException(
                    sprintf(
                        'Method %s::%s() does not have the same number of required parameters in interface: "%s"',
                        $classDefinition->getCompleteName(),
                        $method->getName(),
                        $interfaceDefinition->getCompleteName()
                    )
                );
            }
        }
    }

    /**
     * Pre-compiles a class/interface gathering method information required by other methods.
     *
     * @param CompilationContext $compilationContext
     *
     * @throws CompilerException
     */
    public function preCompile(CompilationContext $compilationContext)
    {
        $this->compiler = $compilationContext->compiler;

        /*
         * Pre-Compile methods
         */
        foreach ($this->methods as $method) {
            if ('class' == $this->getType() && !$method->isAbstract()) {
                $method->preCompile($compilationContext);
            }
        }
    }

    /**
     * Returns the initialization method if any does exist.
     *
     * @return ClassMethod
     */
    public function getInitMethod()
    {
        $initClassName = $this->getCNamespace().'_'.$this->getName();

        return $this->getMethod('zephir_init_properties_'.$initClassName);
    }

    /**
     * Returns the initialization method if any does exist.
     *
     * @return ClassMethod
     */
    public function getStaticInitMethod()
    {
        $initClassName = $this->getCNamespace().'_'.$this->getName();

        return $this->getMethod('zephir_init_static_properties_'.$initClassName);
    }

    /**
     * Returns the initialization method if any does exist.
     *
     * @return ClassMethod
     */
    public function getLocalOrParentInitMethod()
    {
        $method = $this->getInitMethod();
        if ($method) {
            $parentClassDefinition = $this->getExtendsClassDefinition();
            if ($parentClassDefinition instanceof self) {
                $method = $parentClassDefinition->getInitMethod();
                if ($method) {
                    $this->addInitMethod($method->getStatementsBlock());
                }
            }
        }

        return $method;
    }

    /**
     * Creates the initialization method.
     *
     * @param StatementsBlock $statementsBlock
     */
    public function addInitMethod(StatementsBlock $statementsBlock)
    {
        if (!$statementsBlock->isEmpty()) {
            $initClassName = $this->getCNamespace().'_'.$this->getName();

            $classMethod = new ClassMethod(
                $this,
                ['internal'],
                'zephir_init_properties_'.$initClassName,
                null,
                $statementsBlock
            );

            $classMethod->setIsInitializer(true);
            $this->addMethod($classMethod);
        }
    }

    /**
     * Creates the static initialization method.
     *
     * @param StatementsBlock $statementsBlock
     */
    public function addStaticInitMethod(StatementsBlock $statementsBlock)
    {
        $initClassName = $this->getCNamespace().'_'.$this->getName();

        $classMethod = new ClassMethod(
            $this,
            ['internal'],
            'zephir_init_static_properties_'.$initClassName,
            null,
            $statementsBlock
        );

        $classMethod->setIsInitializer(true);
        $classMethod->setIsStatic(true);
        $this->addMethod($classMethod);
    }

    /**
     * Compiles a class/interface.
     *
     * @param CompilationContext $compilationContext
     *
     * @throws CompilerException
     * @throws Exception
     */
    public function compile(CompilationContext $compilationContext)
    {
        $this->compiler = $compilationContext->compiler;

        /*
         * Sets the current object as global class definition
         */
        $compilationContext->classDefinition = $this;

        /*
         * Get the global codePrinter.
         */
        $codePrinter = $compilationContext->codePrinter;

        /*
         * The ZEPHIR_INIT_CLASS defines properties and constants exported by the class.
         */
        $initClassName = $this->getCNamespace().'_'.$this->getName();
        $codePrinter->output('ZEPHIR_INIT_CLASS('.$initClassName.') {');
        $codePrinter->outputBlankLine();

        $codePrinter->increaseLevel();

        /*
         * Method entry.
         */
        $methods = &$this->methods;
        $initMethod = $this->getLocalOrParentInitMethod();

        if (\count($methods) || $initMethod) {
            $methodEntry = strtolower($this->getCNamespace()).'_'.strtolower($this->getName()).'_method_entry';
        } else {
            $methodEntry = 'NULL';
        }

        foreach ($methods as $method) {
            $method->setupOptimized($compilationContext);
        }

        $namespace = str_replace('\\', '_', $compilationContext->config->get('namespace'));

        $flags = '0';
        if ($this->isAbstract()) {
            $flags = 'ZEND_ACC_EXPLICIT_ABSTRACT_CLASS';
        }
        if ($this->isFinal()) {
            if ('0' == $flags) {
                $flags = 'ZEND_ACC_FINAL_CLASS';
            } else {
                $flags .= '|ZEND_ACC_FINAL_CLASS';
            }
        }

        /*
         * Register the class with extends + interfaces.
         */
        $classExtendsDefinition = null;
        if ($this->extendsClass) {
            $classExtendsDefinition = $this->extendsClassDefinition;
            if ($classExtendsDefinition instanceof self && !$classExtendsDefinition->isBundled()) {
                $classEntry = $classExtendsDefinition->getClassEntry($compilationContext);
            } else {
                $classEntry = $this->getClassEntryByClassName($classExtendsDefinition->getName(), $compilationContext);
            }

            if ('class' == $this->getType()) {
                $codePrinter->output('ZEPHIR_REGISTER_CLASS_EX('.$this->getNCNamespace().', '.$this->getName().', '.$namespace.', '.strtolower($this->getSCName($namespace)).', '.$classEntry.', '.$methodEntry.', '.$flags.');');
                $codePrinter->outputBlankLine();
            } else {
                $codePrinter->output('ZEPHIR_REGISTER_INTERFACE_EX('.$this->getNCNamespace().', '.$this->getName().', '.$namespace.', '.strtolower($this->getSCName($namespace)).', '.$classEntry.', '.$methodEntry.');');
                $codePrinter->outputBlankLine();
            }
        } else {
            if ('class' == $this->getType()) {
                $codePrinter->output('ZEPHIR_REGISTER_CLASS('.$this->getNCNamespace().', '.$this->getName().', '.$namespace.', '.strtolower($this->getSCName($namespace)).', '.$methodEntry.', '.$flags.');');
            } else {
                $codePrinter->output('ZEPHIR_REGISTER_INTERFACE('.$this->getNCNamespace().', '.$this->getName().', '.$namespace.', '.strtolower($this->getSCName($namespace)).', '.$methodEntry.');');
            }
            $codePrinter->outputBlankLine();
        }

        /*
         * Compile properties.
         */
        foreach ($this->getProperties() as $property) {
            $docBlock = $property->getDocBlock();
            if ($docBlock) {
                $codePrinter->outputDocBlock($docBlock, true);
            }

            $property->compile($compilationContext);
            $codePrinter->outputBlankLine();
        }

        $initMethod = $this->getInitMethod();
        if ($initMethod) {
            $codePrinter->output($namespace.'_'.strtolower($this->getSCName($namespace)).'_ce->create_object = '.$initMethod->getName().';');
        }

        /*
         * Compile constants.
         */
        foreach ($this->getConstants() as $constant) {
            $docBlock = $constant->getDocBlock();
            if ($docBlock) {
                $codePrinter->outputDocBlock($docBlock, true);
            }

            $constant->compile($compilationContext);
            $codePrinter->outputBlankLine();
        }

        /**
         * Implemented interfaces.
         */
        $interfaces = $this->interfaces;
        $compiler = $compilationContext->compiler;

        if (\is_array($interfaces)) {
            $codePrinter->outputBlankLine(true);

            foreach ($interfaces as $interface) {
                /**
                 * Try to find the interface.
                 */
                $classEntry = false;

                if ($compiler->isInterface($interface)) {
                    $classInterfaceDefinition = $compiler->getClassDefinition($interface);
                    $classEntry = $classInterfaceDefinition->getClassEntry($compilationContext);
                } else {
                    if ($compiler->isBundledInterface($interface)) {
                        $classInterfaceDefinition = $compiler->getInternalClassDefinition($interface);
                        $classEntry = $this->getClassEntryByClassName($classInterfaceDefinition->getName(), $compilationContext);
                    }
                }

                if (!$classEntry) {
                    if ($compiler->isClass($interface)) {
                        throw new CompilerException(
                            sprintf(
                                'Cannot locate interface %s when implementing interfaces on %s. '.
                                '%s is currently a class',
                                $interface,
                                $this->getCompleteName(),
                                $interface
                            ),
                            $this->originalNode
                        );
                    } else {
                        throw new CompilerException(
                            sprintf(
                                'Cannot locate interface %s when implementing interfaces on %s',
                                $interface,
                                $this->getCompleteName()
                            ),
                            $this->originalNode
                        );
                    }
                }

                /*
                 * We don't check if abstract classes implement the methods in their interfaces
                 */
                if (!$this->isAbstract() && !$this->isInterface()) {
                    $this->checkInterfaceImplements($this, $classInterfaceDefinition);
                }

                $codePrinter->output(sprintf(
                    'zend_class_implements(%s, 1, %s);',
                    $this->getClassEntry(),
                    $classEntry
                ));
            }
        }

        if (!$this->isAbstract() && !$this->isInterface()) {
            /*
             * Interfaces in extended classes may have
             */
            if ($classExtendsDefinition) {
                if ($classExtendsDefinition instanceof self && !$classExtendsDefinition->isBundled()) {
                    $interfaces = $classExtendsDefinition->getImplementedInterfaces();
                    if (\is_array($interfaces)) {
                        foreach ($interfaces as $interface) {
                            $classInterfaceDefinition = null;
                            if ($compiler->isInterface($interface)) {
                                $classInterfaceDefinition = $compiler->getClassDefinition($interface);
                            } else {
                                if ($compiler->isBundledInterface($interface)) {
                                    $classInterfaceDefinition = $compiler->getInternalClassDefinition($interface);
                                }
                            }

                            if ($classInterfaceDefinition) {
                                $this->checkInterfaceImplements($this, $classInterfaceDefinition);
                            }
                        }
                    }
                }
            }
        }

        $codePrinter->output('return SUCCESS;');

        $codePrinter->outputBlankLine();
        $codePrinter->decreaseLevel();

        $codePrinter->output('}');
        $codePrinter->outputBlankLine();

        /*
         * Compile methods
         */
        foreach ($methods as $method) {
            $docBlock = $method->getDocBlock();
            if ($docBlock) {
                $codePrinter->outputDocBlock($docBlock);
            }

            if ('class' == $this->getType()) {
                if (!$method->isInternal()) {
                    $codePrinter->output('PHP_METHOD('.$this->getCNamespace().'_'.$this->getName().', '.$method->getName().') {');
                } else {
                    $codePrinter->output($compilationContext->backend->getInternalSignature($method, $compilationContext).' {');
                }
                $codePrinter->outputBlankLine();

                if (!$method->isAbstract()) {
                    $method->compile($compilationContext);
                }

                $codePrinter->output('}');
                $codePrinter->outputBlankLine();
            } else {
                $codePrinter->output('ZEPHIR_DOC_METHOD('.$this->getCNamespace().'_'.$this->getName().', '.$method->getName().');');
                $codePrinter->outputBlankLine();
            }
        }

        /**
         * Check whether classes must be exported.
         */
        $exportClasses = $compilationContext->config->get('export-classes', 'extra');
        if ($exportClasses) {
            $exportAPI = 'extern ZEPHIR_API';
        } else {
            $exportAPI = 'extern';
        }

        /**
         * Create a code printer for the header file.
         */
        $codePrinter = new CodePrinter();

        $codePrinter->outputBlankLine();
        $codePrinter->output($exportAPI.' zend_class_entry *'.$this->getClassEntry().';');
        $codePrinter->outputBlankLine();

        $codePrinter->output('ZEPHIR_INIT_CLASS('.$this->getCNamespace().'_'.$this->getName().');');
        $codePrinter->outputBlankLine();

        if ('class' == $this->getType()) {
            if (\count($methods)) {
                foreach ($methods as $method) {
                    if (!$method->isInternal()) {
                        $codePrinter->output('PHP_METHOD('.$this->getCNamespace().'_'.$this->getName().', '.$method->getName().');');
                    } else {
                        $internalSignature = $compilationContext->backend->getInternalSignature($method, $compilationContext);
                        $codePrinter->output($internalSignature.';');
                    }
                }
                $codePrinter->outputBlankLine();
            }
        }

        /*
         * Specifying Argument Information
         */
        foreach ($methods as $method) {
            $argInfo = new ArgInfoDefinition(
                $method->getArgInfoName($this),
                $method,
                $codePrinter,
                $compilationContext
            );

            $argInfo->setBooleanDefinition($this->compiler->backend->isZE3() ? '_IS_BOOL' : 'IS_BOOL');
            $argInfo->setRichFormat($this->compiler->backend->isZE3());

            $argInfo->render();
        }

        if (\count($methods)) {
            $codePrinter->output(
                sprintf(
                    'ZEPHIR_INIT_FUNCS(%s_%s_method_entry) {',
                    strtolower($this->getCNamespace()),
                    strtolower($this->getName())
                )
            );

            foreach ($methods as $method) {
                if ('class' == $this->getType()) {
                    if (!$method->isInternal()) {
                        $richFormat = $this->compiler->backend->isZE3() &&
                            $method->isReturnTypesHintDetermined() &&
                            $method->areReturnTypesCompatible();

                        if ($richFormat || $method->hasParameters()) {
                            $codePrinter->output(
                                sprintf(
                                    "\tPHP_ME(%s_%s, %s, %s, %s)",
                                    $this->getCNamespace(),
                                    $this->getName(),
                                    $method->getName(),
                                    $method->getArgInfoName($this),
                                    $method->getModifiers()
                                )
                            );
                        } else {
                            $codePrinter->output(
                                sprintf(
                                    "\tPHP_ME(%s_%s, %s, NULL, %s)",
                                    $this->getCNamespace(),
                                    $this->getName(),
                                    $method->getName(),
                                    $method->getModifiers()
                                )
                            );
                        }
                    }
                } else {
                    $richFormat = $this->compiler->backend->isZE3() &&
                            $method->isReturnTypesHintDetermined() &&
                            $method->areReturnTypesCompatible();

                    if ($method->isStatic()) {
                        if ($richFormat || $method->hasParameters()) {
                            $codePrinter->output(
                                sprintf(
                                    "\tZEND_FENTRY(%s, NULL, %s, ZEND_ACC_STATIC|ZEND_ACC_ABSTRACT|ZEND_ACC_PUBLIC)",
                                    $method->getName(),
                                    $method->getArgInfoName($this)
                                )
                            );
                        } else {
                            $codePrinter->output(
                                sprintf(
                                    "\tZEND_FENTRY(%s, NULL, NULL, ZEND_ACC_STATIC|ZEND_ACC_ABSTRACT|ZEND_ACC_PUBLIC)",
                                    $method->getName()
                                )
                            );
                        }
                    } else {
                        if ($richFormat || $method->hasParameters()) {
                            $codePrinter->output(
                                sprintf(
                                    "\tPHP_ABSTRACT_ME(%s_%s, %s, %s)",
                                    $this->getCNamespace(),
                                    $this->getName(),
                                    $method->getName(),
                                    $method->getArgInfoName($this)
                                )
                            );
                        } else {
                            $codePrinter->output(
                                sprintf(
                                    "\tPHP_ABSTRACT_ME(%s_%s, %s, NULL)",
                                    $this->getCNamespace(),
                                    $this->getName(),
                                    $method->getName()
                                )
                            );
                        }
                    }
                }
            }
            $codePrinter->output("\t".'PHP_FE_END');
            $codePrinter->output('};'); // ZEPHIR_INIT_FUNCS
        }

        $compilationContext->headerPrinter = $codePrinter;
    }

    /**
     * @return AliasManager
     */
    public function getAliasManager()
    {
        return $this->aliasManager;
    }

    /**
     * @param AliasManager $aliasManager
     */
    public function setAliasManager(AliasManager $aliasManager)
    {
        $this->aliasManager = $aliasManager;
    }

    /**
     * Convert Class/Interface name to C ClassEntry.
     *
     * @param string             $className
     * @param CompilationContext $compilationContext
     * @param bool               $check
     *
     * @throws CompilerException
     *
     * @return string
     */
    public function getClassEntryByClassName($className, CompilationContext $compilationContext, $check = true)
    {
        $this->compiler = $compilationContext->compiler;

        switch (strtolower($className)) {
            /*
             * Zend classes
             */
            case 'exception':
                $classEntry = 'zend_exception_get_default(TSRMLS_C)';
                break;

            /*
             * Zend interfaces (Zend/zend_interfaces.h)
             */
            case 'iterator':
                $classEntry = 'zend_ce_iterator';
                break;

            case 'arrayaccess':
                $classEntry = 'zend_ce_arrayaccess';
                break;

            case 'serializable':
                $classEntry = 'zend_ce_serializable';
                break;

            case 'iteratoraggregate':
                $classEntry = 'zend_ce_aggregate';
                break;

            /*
             * SPL Exceptions
             */
            case 'logicexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_LogicException';
                break;

            case 'badfunctioncallexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_BadFunctionCallException';
                break;

            case 'badmethodcallexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_BadMethodCallException';
                break;

            case 'domainexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_DomainException';
                break;

            case 'invalidargumentexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_InvalidArgumentException';
                break;

            case 'lengthexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_LengthException';
                break;

            case 'outofrangeexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_OutOfRangeException';
                break;

            case 'runtimeexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_RuntimeException';
                break;

            case 'outofboundsexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_OutOfBoundsException';
                break;

            case 'overflowexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_OverflowException';
                break;

            case 'rangeexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_RangeException';
                break;

            case 'underflowexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_UnderflowException';
                break;

            case 'unexpectedvalueexception':
                $compilationContext->headersManager->add('ext/spl/spl_exceptions');
                $classEntry = 'spl_ce_UnexpectedValueException';
                break;

            /*
             * SPL Iterators Interfaces (spl/spl_iterators.h)
             */
            case 'recursiveiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RecursiveIterator';
                break;

            case 'recursiveiteratoriterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RecursiveIteratorIterator';
                break;

            case 'recursivetreeiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RecursiveTreeIterator';
                break;

            case 'filteriterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_FilterIterator';
                break;

            case 'recursivefilteriterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RecursiveFilterIterator';
                break;

            case 'parentiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_ParentIterator';
                break;

            case 'seekableiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_SeekableIterator';
                break;

            case 'limititerator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_LimitIterator';
                break;

            case 'cachingiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_CachingIterator';
                break;

            case 'recursivecachingiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RecursiveCachingIterator';
                break;

            case 'outeriterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_OuterIterator';
                break;

            case 'iteratoriterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_IteratorIterator';
                break;

            case 'norewinditerator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_NoRewindIterator';
                break;

            case 'infiniteiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_InfiniteIterator';
                break;

            case 'emptyiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_EmptyIterator';
                break;

            case 'appenditerator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_AppendIterator';
                break;

            case 'regexiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RegexIterator';
                break;

            case 'recursiveregexiterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RecursiveRegexIterator';
                break;

            case 'directoryiterator':
                $compilationContext->headersManager->add('ext/spl/spl_directory');
                $classEntry = 'spl_ce_DirectoryIterator';
                break;

            case 'filesystemiterator':
                $compilationContext->headersManager->add('ext/spl/spl_directory');
                $classEntry = 'spl_ce_FilesystemIterator';
                break;

            case 'recursivedirectoryiterator':
                $compilationContext->headersManager->add('ext/spl/spl_directory');
                $classEntry = 'spl_ce_RecursiveDirectoryIterator';
                break;

            case 'globiterator':
                $compilationContext->headersManager->add('ext/spl/spl_directory');
                $classEntry = 'spl_ce_GlobIterator';
                break;

            case 'splfileobject':
                $compilationContext->headersManager->add('ext/spl/spl_directory');
                $classEntry = 'spl_ce_SplFileObject';
                break;

            case 'spltempfileobject':
                $compilationContext->headersManager->add('ext/spl/spl_directory');
                $classEntry = 'spl_ce_SplTempFileObject';
                break;

            case 'countable':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_Countable';
                break;

            case 'callbackfilteriterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_CallbackFilterIterator';
                break;

            case 'recursivecallbackfilteriterator':
                $compilationContext->headersManager->add('ext/spl/spl_iterators');
                $classEntry = 'spl_ce_RecursiveCallbackFilterIterator';
                break;

            case 'arrayobject':
                $compilationContext->headersManager->add('ext/spl/spl_array');
                $classEntry = 'spl_ce_ArrayObject';
                break;

            case 'splfixedarray':
                $compilationContext->headersManager->add('ext/spl/spl_fixedarray');
                $classEntry = 'spl_ce_SplFixedArray';
                break;

            case 'splpriorityqueue':
                $compilationContext->headersManager->add('ext/spl/spl_heap');
                $classEntry = 'spl_ce_SplPriorityQueue';
                break;

            case 'splfileinfo':
                $compilationContext->headersManager->add('ext/spl/spl_directory');
                $classEntry = 'spl_ce_SplFileInfo';
                break;

            case 'splheap':
                $compilationContext->headersManager->add('ext/spl/spl_heap');
                $classEntry = 'spl_ce_SplHeap';
                break;

            case 'splminheap':
                $compilationContext->headersManager->add('ext/spl/spl_heap');
                $classEntry = 'spl_ce_SplMinHeap';
                break;

            case 'splmaxheap':
                $compilationContext->headersManager->add('ext/spl/spl_heap');
                $classEntry = 'spl_ce_SplMaxHeap';
                break;

            case 'splstack':
                $compilationContext->headersManager->add('ext/spl/spl_dllist');
                $classEntry = 'spl_ce_SplStack';
                break;

            case 'splqueue':
                $compilationContext->headersManager->add('ext/spl/spl_dllist');
                $classEntry = 'spl_ce_SplQueue';
                break;

            case 'spldoublylinkedlist':
                $compilationContext->headersManager->add('ext/spl/spl_dllist');
                $classEntry = 'spl_ce_SplDoublyLinkedList';
                break;

            case 'stdclass':
                $classEntry = 'zend_standard_class_def';
                break;

            case 'closure':
                $compilationContext->headersManager->add('Zend/zend_closures');
                $classEntry = 'zend_ce_closure';
                break;

            case 'pdo':
                $compilationContext->headersManager->add('ext/pdo/php_pdo_driver');
                $classEntry = 'php_pdo_get_dbh_ce()';
                break;

            case 'pdostatement':
                $compilationContext->headersManager->add('kernel/main');
                $classEntry = $compilationContext->backend->fetchClassEntry('pdostatement');
                break;

            case 'pdoexception':
                $compilationContext->headersManager->add('ext/pdo/php_pdo_driver');
                $classEntry = 'php_pdo_get_exception()';
                break;

            case 'datetime':
                $compilationContext->headersManager->add('ext/date/php_date');
                $classEntry = 'php_date_get_date_ce()';
                break;

            case 'datetimezone':
                $compilationContext->headersManager->add('ext/date/php_date');
                $classEntry = 'php_date_get_timezone_ce()';
                break;

            // Reflection
            /*case 'reflector':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflector_ptr';
                break;
            case 'reflectionexception':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_exception_ptr';
                break;
            case 'reflection':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_ptr';
                break;
            case 'reflectionfunctionabstract':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_function_abstract_ptr';
                break;
            case 'reflectionfunction':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_function_ptr';
                break;
            case 'reflectionparameter':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_parameter_ptr';
                break;
            case 'reflectionclass':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_class_ptr';
                break;
            case 'reflectionobject':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_object_ptr';
                break;
            case 'reflectionmethod':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_method_ptr';
                break;
            case 'reflectionproperty':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_property_ptr';
                break;
            case 'reflectionextension':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_extension_ptr';
                break;
            case 'reflectionzendextension':
                $compilationContext->headersManager->add('ext/reflection/php_reflection');
                $classEntry = 'reflection_zend_extension_ptr';
                break;*/

            default:
                if (!$check) {
                    throw new CompilerException('Unknown class entry for "'.$className.'"');
                } else {
                    $classEntry = $compilationContext->backend->fetchClassEntry(escape_class(strtolower($className)));
                }
        }

        return $classEntry;
    }

    /**
     * Builds a class definition from reflection.
     *
     * @param \ReflectionClass $class
     *
     * @return ClassDefinition
     */
    public static function buildFromReflection(\ReflectionClass $class)
    {
        $classDefinition = new self($class->getNamespaceName(), $class->getName(), $class->getShortName());

        $methods = $class->getMethods();
        if (\count($methods) > 0) {
            foreach ($methods as $method) {
                $parameters = [];

                foreach ($method->getParameters() as $row) {
                    $params = [
                        'type' => 'parameter',
                        'name' => $row->getName(),
                        'const' => 0,
                        'data-type' => 'variable',
                        'mandatory' => !$row->isOptional(),
                    ];
                    if (!$params['mandatory']) {
                        try {
                            $params['default'] = $row->getDefaultValue();
                        } catch (\ReflectionException $e) {
                            // TODO: dummy default value
                            $params['default'] = true;
                        }
                    }
                    $parameters[] = $params;
                }

                $classMethod = new ClassMethod(
                    $classDefinition,
                    [],
                    $method->getName(),
                    new ClassMethodParameters($parameters)
                );
                $classMethod->setIsStatic($method->isStatic());
                $classMethod->setIsBundled(true);
                $classDefinition->addMethod($classMethod);
            }
        }

        $constants = $class->getConstants();
        if (\count($constants) > 0) {
            foreach ($constants as $constantName => $constantValue) {
                $type = self::_convertPhpConstantType(\gettype($constantValue));
                $classConstant = new ClassConstant($constantName, ['value' => $constantValue, 'type' => $type], null);
                $classDefinition->addConstant($classConstant);
            }
        }

        $properties = $class->getProperties();
        if (\count($properties) > 0) {
            foreach ($properties as $property) {
                $visibility = [];

                if ($property->isPublic()) {
                    $visibility[] = 'public';
                }

                if ($property->isPrivate()) {
                    $visibility[] = 'private';
                }

                if ($property->isProtected()) {
                    $visibility[] = 'protected';
                }

                if ($property->isStatic()) {
                    $visibility[] = 'static';
                }

                $classProperty = new ClassProperty(
                    $classDefinition,
                    $visibility,
                    $property->getName(),
                    null,
                    null,
                    null
                );
                $classDefinition->addProperty($classProperty);
            }
        }

        $classDefinition->setIsBundled(true);

        return $classDefinition;
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    protected function hasConstantFromInterfaces($name)
    {
        if ($interfaces = $this->getImplementedInterfaceDefinitions()) {
            foreach ($interfaces as $interface) {
                if ($interface->hasConstant($name)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @param string $name
     *
     * @return bool|ClassConstant
     */
    protected function getConstantFromInterfaces($name)
    {
        if ($interfaces = $this->getImplementedInterfaceDefinitions()) {
            foreach ($interfaces as $interface) {
                if ($interface->hasConstant($name)) {
                    return $interface->getConstant($name);
                }
            }
        }

        return false;
    }

    private static function _convertPhpConstantType($phpType)
    {
        $map = [
            'boolean' => 'bool',
            'integer' => 'int',
            'double' => 'double',
            'string' => 'string',
            'NULL' => 'null',
        ];

        if (!isset($map[$phpType])) {
            throw new CompilerException("Cannot parse constant type '$phpType'");
        }

        return $map[$phpType];
    }
}
